package com.atguigu.gulimall.product.web;

import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Controller
@Slf4j
public class IndexController {

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private RedissonClient redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    //访问 / 或者 /index.html 都是访问首页
    @GetMapping({"/","/index.html"})
    public String indexPage(Model model){

        //查出所有一级分类
        List<CategoryEntity> categoryEntities = this.categoryService.getLevel1Categorys();

        //将数据转发给页面
        model.addAttribute("categorys",categoryEntities);

        //thymeleaf有默认前缀 classpath:/templates/ 和默认后缀 .html
        //classpath代表类路径
        //视图解析器会使用返回值进行拼串 classpath:/templates/ + index + .html
        return "index";
    }

    @GetMapping("/index/catalog.json")
    @ResponseBody
    public Map<String, List<Catelog2Vo>> getCatalogJson(){
        Map<String, List<Catelog2Vo>> map = this.categoryService.getCatalogJson();
        log.info("-->请求成功");
        return map;
    }

    /*
        Redisson看门狗机制
            (1)锁的自动续期,如果业务执行时间超长,看门狗会在锁即将到期时自动给锁续上新的30s,这样就不用担心由于业务执行
                时间过长,导致锁自动过期被删掉
            (2)加锁的业务只要运行完成,看门狗就不会给锁进行续期,这样即使不手动解锁,锁也会在30s后自动删除,避免死锁问题
     */
    @GetMapping("/hello")
    @ResponseBody
    public String hello(){
        //1、获取一把锁,锁名称自定义,只要名称相同就是同一把锁
        RLock lock = this.redisson.getLock("my-lock");

        //2、加锁
        //阻塞式等待,意思是如果一个线程加不到锁,它就会停在这一步一直等待,直到它拿到锁才会继续向下执行
        //默认锁的过期时间是30s,但有看门狗机制进行自动续期
//        lock.lock();

        //在加锁的同时设置过期时间,10秒以后自动解锁
        //使用这种设置方式,便不再有看门狗机制,即锁时间到了以后不会自动续期
        lock.lock(10, TimeUnit.SECONDS);

        //如果指定了锁的超时时间,redisson底层就给redis执行lua脚本进行占锁,默认超时时间就是指定的时间
        //如果没有指定锁的超时时间,就使用看门狗的默认时间(30s),只要占锁成功,就会启动一个定时任务,定时给锁
        //重新设置过期时间(30s),看门狗默认每10s续期一次,即将过期时间重新设置为30s
        //实际操作中,推荐使用指定锁的过期时间,即第二种方式,可以省掉自动续期的操作,在操作完成后手动解锁
        try{
            log.info("加锁成功,执行业务..." + Thread.currentThread().getId());
            //模拟业务执行很长时间
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //由于业务可能出现问题,所以将解锁放到finally内,保证解锁始终能运行
            log.info("--->释放锁" + Thread.currentThread().getId());

            //即使没有解锁,看门狗机制也会自动释放锁
            //3、解锁
            lock.unlock();
        }
        return "hello";
    }

    /*
        读写锁
            用法: 改数据加写锁,读数据加读锁
            作用: 保证一定能读到最新的数据
            原理: 写锁是一个排他锁(互拆锁、独享锁),读锁是一个共享锁
                如果有多个读同时操作,那么相当于无锁,并发读,会在redis中记录好锁,所有当前的读锁都会同时加锁成功
                如果读的过程中进来了写操作,那么写操作必须等读锁释放后才能继续操作
                如果有多个写操作,那么只能排队依次操作
                如果写的过程中有读,那么必须等写完了才能读
    */
    @GetMapping("/write")
    @ResponseBody
    public String writeValue(){
        //读写锁
        RReadWriteLock readWriteLock = this.redisson.getReadWriteLock("rw-lock");
        //写锁
        RLock rLock = readWriteLock.writeLock();

        String uuid = "";
        //模拟写操作,在30秒后向 redis 中写入一个uuid
        try {
            //执行加锁
            rLock.lock();
            log.info("写锁加锁成功 ->" + Thread.currentThread().getId());

            uuid = UUID.randomUUID().toString();
            Thread.sleep(30000);
            this.stringRedisTemplate.opsForValue().set("writeValue",uuid);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //解锁
            rLock.unlock();
            log.info("写锁释放成功 ->" + Thread.currentThread().getId());
        }
        return uuid;
    }

    @GetMapping("/read")
    @ResponseBody
    public String readValue(){
        //读写锁
        RReadWriteLock readWriteLock = this.redisson.getReadWriteLock("rw-lock");
        //读锁
        RLock rLock = readWriteLock.readLock();
        //执行加锁
        rLock.lock();

        String writeValue = "";
        try{
            log.info("读锁加锁成功 ->" + Thread.currentThread().getId());
            //从 redis 中获取值
            writeValue = this.stringRedisTemplate.opsForValue().get("writeValue");
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //解锁
            rLock.unlock();
            log.info("读锁释放成功 ->" + Thread.currentThread().getId());
        }

        return writeValue;
    }

    /*
        信号量
            场景: 车库停车,现在有3个车位,当有车离开则释放一个车位,有车进来就看有没有空余车位
            首先在 redis 中设置一个键值对, key = park value = 3
            信号量可以用于分布式系统的限流操作
    */
    //车库停车
    @GetMapping("/park")
    @ResponseBody
    public String park() throws InterruptedException {
        RSemaphore park = this.redisson.getSemaphore("park");//同一个名称则认为是同一个信号量

        //获取一个信号,获取到了则继续执行,没获取到则在此等待,是一个阻塞式方法
        //相当于占一个车位
//        park.acquire();

        //尝试获取一个信号,并返回获取结果,非阻塞式
        boolean result = park.tryAcquire();

        return "ok:" + result;
    }

    //从车库开车走
    @GetMapping("/go")
    @ResponseBody
    public String go() throws InterruptedException {
        RSemaphore park = this.redisson.getSemaphore("park");//同一个名称则认为是同一个信号量

        //释放一个车位
        park.release();

        return "ok";
    }

    /*
        闭锁
            场景: 学校内所有班级的人全部走完,学校才可以锁门
    */
    @GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {
        //闭锁
        RCountDownLatch door = this.redisson.getCountDownLatch("door");//同一个名称则认为是同一个闭锁

        //等待 5 个班的人都走完
        door.trySetCount(5);

        //等待闭锁完成
        door.await();

        return "锁门成功!";
    }

    @GetMapping("/go/{id}")
    @ResponseBody
    public String go(@PathVariable("id") Long id){
        //闭锁
        RCountDownLatch door = this.redisson.getCountDownLatch("door"); //同一个名称则认为是同一个闭锁

        //计数减一,相当于走一个人,那么学校里就少一个班
        door.countDown();

        return id + "班的人都走了...";
    }

}
